/**
 * Common Utils Module based on the native common_utils_bridge C library.
 *
 * This module provides a collection of common utilities organized into namespaces.
 * It is designed as a stateless, singleton-like utility library.
 *
 * Features:
 * - `crypto`: Hashing (MD5, HMAC-MD5), symmetric encryption (AES),
 *             and asymmetric encryption (RSA).
 * - `fs`: File system operations, such as converting files to/from Base64.
 * - `codec`: Data encoding and decoding functions (Hex, Base64, UTF-8, etc.).
 * - `random`: Generation of cryptographically secure random bytes and simple
 *           random strings.
 *
 * Usage:
 * import dxCommonUtils from './dxCommonUtils.js';
 * const md5Hash = dxCommonUtils.crypto.md5('hello');
 */
import { commonUtilsClass } from './libvbar-m-dxcommonutils.so'

const dxCommonUtils = {};

// ----------- Constants & Enums -------------------
dxCommonUtils.AES_MODE = {
    ECB: 'ECB',
    CBC: 'CBC',
    CFB: 'CFB',
    OFB: 'OFB',
    CTR: 'CTR'
};

dxCommonUtils.AES_KEY_SIZE = {
    BITS_128: 128,
    BITS_192: 192,
    BITS_256: 256
};

dxCommonUtils.AES_PADDING = {
    PKCS7: 'PKCS7',
    NONE: 'NoPadding'
};

dxCommonUtils.RSA_KEY_SIZE = {
    BITS_1024: 1024,
    BITS_2048: 2048,
    BITS_4096: 4096
};

const utils = new commonUtilsClass();

// ----------- Crypto Namespace -------------------
dxCommonUtils.crypto = {};
dxCommonUtils.crypto.aes = {};
dxCommonUtils.crypto.rsa = {};

/**
 * Calculates MD5 hash of the input string.
 * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8.
 * @returns {string} MD5 hash in hexadecimal format.
 */
dxCommonUtils.crypto.md5 = function (data) {
    const buffer = _normalizeDataToBuffer(data, false, 'Data');
    return utils.md5(buffer);
}

/**
 * Calculates HMAC-MD5 hash using the provided key.
 * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8.
 * @param {string|ArrayBuffer|Uint8Array} key - The secret key for HMAC. If a string is provided, it will be treated as UTF-8.
 * @returns {string} HMAC-MD5 hash in hexadecimal format.
 */
dxCommonUtils.crypto.hmacMd5 = function (data, key) {
    const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');
    const keyBuffer = _normalizeDataToBuffer(key, false, 'Key');
    return utils.hmacMd5(dataBuffer, keyBuffer);
}

/**
 * Calculates a hash of the input data using the specified algorithm.
 * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8.
 * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm to use (e.g., 'SHA-256', 'MD5', 'SHA1', 'SHA-384', 'SHA-512').
 * @returns {string} The hash in hexadecimal format.
 */
dxCommonUtils.crypto.hash = function (data, hashAlgorithm = 'SHA-256') {
    const buffer = _normalizeDataToBuffer(data, false, 'Data');
    return utils.hash(buffer, hashAlgorithm);
}

/**
 * Encrypts data using AES encryption.
 * @param {string|ArrayBuffer|Uint8Array} data - The data to encrypt. If a string is provided, it will be treated as UTF-8.
 * @param {string|ArrayBuffer|Uint8Array} key - The encryption key. Can be a hex string, an ArrayBuffer, or a Uint8Array.
 * @param {object} options - Encryption options.
 * @param {string} [options.mode='CBC'] - The AES mode, from dxCommonUtils.AES_MODE.
 * @param {number} [options.keySize=256] - The key size in bits (128, 192, or 256), from dxCommonUtils.AES_KEY_SIZE.
 * @param {string|ArrayBuffer|Uint8Array} [options.iv] - The initialization vector (required for non-ECB modes). This is ignored if useSalt is true.
 * @param {string} [options.padding='PKCS7'] - The padding scheme, from dxCommonUtils.AES_PADDING. Defaults to PKCS7.
 * @param {boolean} [options.useSalt=false] - If true, generates an OpenSSL-compatible salted ciphertext. The provided 'key' is used as a password (UTF-8 string) to derive the actual key and IV.
 * @returns {string} The encrypted data as a Base64 string.
 */
dxCommonUtils.crypto.aes.encrypt = function (data, key, options = {}) {
    // Set default options, including PKCS7 padding by default
    const finalOptions = Object.assign({
        mode: dxCommonUtils.AES_MODE.CBC,
        keySize: 256,
        padding: dxCommonUtils.AES_PADDING.PKCS7,
        useSalt: false // Default to false for backward compatibility
    }, options);

    let keyBuffer;
    if (finalOptions.useSalt) {
        // In salted mode, a string key is treated as a password (UTF-8).
        keyBuffer = _normalizeDataToBuffer(key, false, 'Key');
    } else {
        // In non-salted mode, a string key is treated as a raw key in hex format.
        keyBuffer = _normalizeHexInput(key, 'Key');

        // Key length must be validated for non-salted keys.
        const expectedKeyLength = finalOptions.keySize / 8;
        if (keyBuffer.byteLength !== expectedKeyLength) {
            throw new Error(`Key length must be ${expectedKeyLength} bytes for ${finalOptions.keySize}-bit AES`);
        }
    }

    // Validate mode and keySize
    if (!Object.values(dxCommonUtils.AES_MODE).includes(finalOptions.mode)) {
        throw new Error("options.mode must be one of dxCommonUtils.AES_MODE values");
    }
    if (!Object.values(dxCommonUtils.AES_KEY_SIZE).includes(finalOptions.keySize)) {
        throw new Error("options.keySize must be one of dxCommonUtils.AES_KEY_SIZE values");
    }

    // Validate padding
    if (finalOptions.padding !== undefined) {
        if (typeof finalOptions.padding !== 'string' || !Object.values(dxCommonUtils.AES_PADDING).includes(finalOptions.padding)) {
            throw new Error("options.padding must be one of dxCommonUtils.AES_PADDING values");
        }
        if (finalOptions.padding === dxCommonUtils.AES_PADDING.NONE) {
            const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');
            if (dataBuffer.byteLength % 16 !== 0) {
                throw new Error("Data length must be a multiple of 16 bytes when using NoPadding");
            }
        }
    }

    const ivBuffer = (finalOptions.iv && !finalOptions.useSalt) ? _normalizeHexInput(finalOptions.iv) : null;
    const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');

    const encrypted = utils.aesEncrypt(dataBuffer, keyBuffer, { ...finalOptions, iv: ivBuffer });
    if (encrypted === null) {
        throw new Error("AES encryption failed. Check parameters.");
    }
    return dxCommonUtils.codec.arrayBufferToBase64(encrypted);
};

/**
 * Decrypts data using AES encryption.
 * Note: This function automatically handles OpenSSL's "Salted__" format if present in the encrypted data.
 * @param {string|ArrayBuffer|Uint8Array} encryptedData - The encrypted data. If a string, it is assumed to be Base64.
 * @param {string|ArrayBuffer|Uint8Array} key - The decryption key. If a string, it will be treated as a password (UTF-8) for salted data, or as a hex string for non-salted data.
 * @param {object} options - Decryption options.
 * @param {string} [options.mode='CBC'] - The AES mode, from dxCommonUtils.AES_MODE.
 * @param {number} [options.keySize=256] - The key size in bits (128, 192, or 256), from dxCommonUtils.AES_KEY_SIZE. This is also used for key derivation in "Salted__" format.
 * @param {string|ArrayBuffer|Uint8Array} [options.iv] - The initialization vector (required for non-ECB modes and non-salted data). If a string, it must be Hex.
 * @param {string} [options.padding='PKCS7'] - The padding scheme, from dxCommonUtils.AES_PADDING. Defaults to PKCS7.
 * @returns {ArrayBuffer|null} The decrypted data as an ArrayBuffer. Returns null on decryption failure (e.g., bad key or padding).
 */
dxCommonUtils.crypto.aes.decrypt = function (encryptedData, key, options = {}) {
    // Set default options, including PKCS7 padding by default
    const finalOptions = Object.assign({
        mode: dxCommonUtils.AES_MODE.CBC,
        keySize: 256,
        padding: dxCommonUtils.AES_PADDING.PKCS7
    }, options);

    // Validate mode and keySize
    if (!Object.values(dxCommonUtils.AES_MODE).includes(finalOptions.mode)) {
        throw new Error("options.mode must be one of dxCommonUtils.AES_MODE values");
    }
    if (!Object.values(dxCommonUtils.AES_KEY_SIZE).includes(finalOptions.keySize)) {
        throw new Error("options.keySize must be one of dxCommonUtils.AES_KEY_SIZE values");
    }

    // Validate padding
    if (finalOptions.padding !== undefined) {
        if (typeof finalOptions.padding !== 'string' || !Object.values(dxCommonUtils.AES_PADDING).includes(finalOptions.padding)) {
            throw new Error("options.padding must be one of dxCommonUtils.AES_PADDING values");
        }
    }

    const encryptedDataBuffer = _normalizeDataToBuffer(encryptedData, true); // true for base64

    // Auto-detect if the data is in OpenSSL's "Salted__" format
    let isSalted = false;
    if (encryptedDataBuffer.byteLength >= 16) {
        const header = new Uint8Array(encryptedDataBuffer, 0, 8);
        // "Salted__" in ASCII: 83 97 108 116 101 100 95 95
        const saltedHeader = new Uint8Array([83, 97, 108, 116, 101, 100, 95, 95]);
        isSalted = header.every((value, index) => value === saltedHeader[index]);
    }

    let keyBuffer;
    if (isSalted) {
        // If data is salted, a string key is treated as a password (UTF-8).
        keyBuffer = _normalizeDataToBuffer(key, false, 'Key');
    } else {
        // If data is not salted, a string key is treated as a raw key in hex format.
        keyBuffer = _normalizeHexInput(key, 'Key');

        // Key length must be validated for non-salted keys.
        const expectedKeyLength = finalOptions.keySize / 8;
        if (keyBuffer.byteLength !== expectedKeyLength) {
            throw new Error(`Key length must be ${expectedKeyLength} bytes for ${finalOptions.keySize}-bit AES`);
        }
    }

    const ivBuffer = finalOptions.iv ? _normalizeHexInput(finalOptions.iv) : null;

    return utils.aesDecrypt(encryptedDataBuffer, keyBuffer, { ...finalOptions, iv: ivBuffer });
};

/**
 * Convenience method for AES-256-CBC encryption with automatic IV generation.
 * The key is treated as a Hex string. The plaintext data is treated as a UTF-8 string.
 * @param {string} data - The UTF-8 data to encrypt.
 * @param {string|ArrayBuffer|Uint8Array} key - The encryption key (32 bytes). If a string, it must be Hex.
 * @returns {{encrypted: string, iv: string}} Object containing Base64 encrypted data and the generated IV as a hex string.
 */
dxCommonUtils.crypto.aes.encryptWithRandomIV = function (data, key) {
    if (typeof data !== 'string') {
        throw new Error('Data must be a UTF-8 string for this convenience function.');
    }

    const keyBuffer = _normalizeHexInput(key, 'Key');
    if (keyBuffer.byteLength !== 32) {
        throw new Error('Key must be 32 bytes for AES-256');
    }

    // 1. Generate 16 random bytes, returned directly as a hex string.
    const ivHex = dxCommonUtils.random.getBytes(16);

    // 2. Encrypt using the main AES function.
    const encrypted = dxCommonUtils.crypto.aes.encrypt(data, keyBuffer, {
        mode: 'CBC',
        keySize: 256,
        iv: ivHex
    });

    // 3. Return the encrypted data and the hex-encoded IV.
    return {
        encrypted: encrypted,
        iv: ivHex
    };
}

/**
 * Generates a new RSA key pair.
 * @param {number} [bits=2048] - Key size in bits: 1024, 2048, or 4096.
 * @returns {object} Object containing privateKey and publicKey in PEM format.
 */
dxCommonUtils.crypto.rsa.generateKeyPair = function (bits = 2048) {
    if (![1024, 2048, 4096].includes(bits)) {
        throw new Error('RSA key size must be 1024, 2048, or 4096 bits');
    }
    return utils.generateRsaKeyPair(bits);
}

/**
 * Encrypts data using RSA public key.
 * @param {string|ArrayBuffer|Uint8Array} data - The data to encrypt. If a string is provided, it will be treated as UTF-8.
 * @param {string} publicKey - PEM formatted RSA public key.
 * @returns {string} Base64 encoded encrypted data.
 */
dxCommonUtils.crypto.rsa.encrypt = function (data, publicKey) {
    const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');

    if (typeof publicKey !== 'string') {
        throw new Error('Public key must be a PEM string');
    }
    if (!publicKey.includes('-----BEGIN PUBLIC KEY-----')) {
        throw new Error('Public key must be in PEM format');
    }
    return utils.rsaEncrypt(dataBuffer, publicKey);
}

/**
 * Decrypts RSA encrypted data using private key.
 * @param {string} encryptedData - Base64 encoded encrypted data.
 * @param {string} privateKey - PEM formatted RSA private key.
 * @returns {ArrayBuffer|null} The decrypted data as an ArrayBuffer. Returns null on decryption failure.
 */
dxCommonUtils.crypto.rsa.decrypt = function (encryptedData, privateKey) {
    if (typeof encryptedData !== 'string') {
        throw new Error('Encrypted data must be a Base64 string');
    }
    if (typeof privateKey !== 'string') {
        throw new Error('Private key must be a PEM string');
    }
    if (!privateKey.includes('-----BEGIN PRIVATE KEY-----')) {
        throw new Error('Private key must be in PEM format');
    }

    // Decode the Base64 input to an ArrayBuffer before passing to native code.
    const encryptedBuffer = dxCommonUtils.codec.base64ToArrayBuffer(encryptedData);

    // Pass the privateKey string directly, as the C layer expects a PEM string.
    const decryptedBuffer = utils.rsaDecrypt(encryptedBuffer, privateKey);
    if (!decryptedBuffer) {
        // Return null instead of throwing to match AES behavior
        return null;
    }

    return decryptedBuffer; // Return the raw ArrayBuffer
};

/**
 * Creates a digital signature for data using an RSA private key.
 * This is the core function needed for standards like JWT (RS256/RS384/RS512).
 * @param {string|ArrayBuffer|Uint8Array} data - The data to sign. If a string, it will be treated as UTF-8.
 * @param {string} privateKey - The PEM formatted RSA private key.
 * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm to use (e.g., 'SHA-256', 'SHA-384', 'SHA-512').
 * @returns {string} The signature as a Base64 encoded string.
 */
dxCommonUtils.crypto.rsa.sign = function (data, privateKey, hashAlgorithm = 'SHA-256') {
    const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');
    if (typeof privateKey !== 'string' || !privateKey.includes('-----BEGIN PRIVATE KEY-----')) {
        throw new Error('Private key must be a PEM string');
    }
    return utils.rsaSign(dataBuffer, privateKey, hashAlgorithm);
};

/**
 * Verifies a digital signature using an RSA public key.
 * This is the counterpart to `rsa.sign` and is used to validate signatures like those in JWT.
 * @param {string|ArrayBuffer|Uint8Array} data - The original, unsigned data.
 * @param {string|ArrayBuffer|Uint8Array} signature - The signature to verify. If a string, it must be Base64 encoded.
 * @param {string} publicKey - The PEM formatted RSA public key.
 * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm used for signing (e.g., 'SHA-256', 'SHA-384', 'SHA-512').
 * @returns {boolean} True if the signature is valid, otherwise false.
 */
dxCommonUtils.crypto.rsa.verify = function (data, signature, publicKey, hashAlgorithm = 'SHA-256') {
    const dataBuffer = _normalizeDataToBuffer(data, false, 'Data');
    // The C layer expects a raw ArrayBuffer, so we decode the Base64 signature here in JS.
    const signatureBuffer = _normalizeDataToBuffer(signature, true, 'Signature');
    if (typeof publicKey !== 'string' || !publicKey.includes('-----BEGIN PUBLIC KEY-----')) {
        throw new Error('Public key must be a PEM string');
    }
    return utils.rsaVerify(dataBuffer, signatureBuffer, publicKey, hashAlgorithm);
};

/**
 * Parses a PEM formatted X.509 certificate and returns its details.
 * @param {string} pemString - The certificate content in PEM format.
 * @returns {object} An object containing certificate details:
 *                   - serialNumber (string)
 *                   - issuer (string)
 *                   - subject (string)
 *                   - validFrom (string)
 *                   - validTo (string)
 *                   - publicKey (string, in PEM format)
 */
dxCommonUtils.crypto.parsePEM = function (pemString) {
    if (typeof pemString !== 'string' || !pemString.includes('-----BEGIN CERTIFICATE-----')) {
        throw new Error('Input must be a PEM formatted certificate string');
    }
    return utils.parsePEMCertificate(pemString);
};

/**
 * Verifies if a certificate was signed by a given Certificate Authority (CA).
 * @param {string} certPEM - The certificate to verify, in PEM format.
 * @param {string} caCertPEM - The CA's certificate, in PEM format.
 * @returns {boolean} True if the certificate is signed by the CA.
 * @throws {Error} If the native verification fails due to parsing errors or signature mismatch.
 */
dxCommonUtils.crypto.verifyCertificate = function (certPEM, caCertPEM) {
    if (typeof certPEM !== 'string' || !certPEM.includes('-----BEGIN CERTIFICATE-----')) {
        throw new Error('Input certPEM must be a PEM formatted certificate string');
    }
    if (typeof caCertPEM !== 'string' || !caCertPEM.includes('-----BEGIN CERTIFICATE-----')) {
        throw new Error('Input caCertPEM must be a PEM formatted certificate string');
    }
    return utils.verifyCertificate(certPEM, caCertPEM);
};

// =====================================================================================
// == FS (File System) Namespace =====================================================
// =====================================================================================
dxCommonUtils.fs = {};

/**
 * Reads the entire content of a file and returns it as a Base64 encoded string.
 * @memberof dxCommonUtils.fs
 * @param {string} filePath - The path to the file.
 * @returns {string} The Base64 encoded content of the file.
 */
dxCommonUtils.fs.fileToBase64 = function (filePath) {
    if (typeof filePath !== 'string' || filePath.length === 0) {
        throw new Error("filePath must be a non-empty string.");
    }
    return utils.fileToBase64(filePath);
}

/**
 * Decodes a Base64 string and writes the binary data to a file.
 * This will overwrite the file if it already exists.
 * @memberof dxCommonUtils.fs
 * @param {string} filePath - The path to the file to be written.
 * @param {string} base64String - The Base64 encoded data.
 * @returns {boolean} Returns true on success.
 */
dxCommonUtils.fs.base64ToFile = function (filePath, base64String) {
    if (typeof filePath !== 'string' || filePath.length === 0) {
        throw new Error("filePath must be a non-empty string.");
    }
    if (typeof base64String !== 'string') {
        throw new Error("base64String must be a string.");
    }
    return utils.base64ToFile(filePath, base64String);
}

// =====================================================================================
// == Random Namespace ===============================================================
// =====================================================================================
dxCommonUtils.random = {};

/**
 * Generates cryptographically secure random bytes.
 * @param {number} length - Number of bytes to generate.
 * @returns {string} Random bytes as a hex string.
 */
dxCommonUtils.random.getBytes = function (length) {
    if (typeof length !== 'number' || length <= 0) {
        throw new Error('Length must be a positive number');
    }
    return utils.generateRandomBytes(length);
}

/**
 * Generates a non-cryptographically secure random string from a given charset.
 * @param {number} length - The length of the string to generate.
 * @param {string} [charset] - The set of characters to use. Defaults to alphanumeric.
 * @returns {string} The generated random string.
 */
dxCommonUtils.random.getStr = function (length, charset) {
    if (typeof length !== 'number' || length <= 0) {
        throw new Error('Length must be a positive number');
    }
    const charSet = charset || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    const charSetLength = charSet.length;
    for (let i = 0; i < length; i++) {
        result += charSet.charAt(Math.floor(Math.random() * charSetLength));
    }
    return result;
}

// ----------- Codec Namespace -------------------
dxCommonUtils.codec = {};

/**
 * Hexadecimal to byte array eg: 313233616263->[49,50,51,97,98,99]
 * @param {string} str A hexadecimal string in lowercase with no space in between
 * @returns {number[]} Digital numbers
 */
dxCommonUtils.codec.hexToBytes = function (str) {
    if (str === undefined || str === null || (typeof str) != 'string' || str.length < 1) {
        throw new Error("dxCommonUtils.codec.hexToBytes:'str' parameter should not be empty")
    }
    if (!/^[0-9a-fA-F]+$/.test(str) || str.length % 2 !== 0) {
        throw new Error("dxCommonUtils.codec.hexToBytes: 'str' parameter must be a valid hex string with an even length");
    }
    let regex = /.{2}/g;
    let arr = str.match(regex);
    return arr.map(item => parseInt(item, 16));
}
/**
 * Byte array to hexadecimal eg: [49,50,51,97,98,99] ->313233616263
 * @param {number[]} numbers Numeric array
 * @returns {string} A hexadecimal string in lowercase with no space in between
 */
dxCommonUtils.codec.bytesToHex = function (numbers) {
    const hexArray = numbers.map(num => num.toString(16).padStart(2, '0').toLowerCase());
    const hexString = hexArray.join('');
    return hexString;
}
/**
 * Hexadecimal to string conversion eg: 313233616263->123abc
 * @description WARNING: This function only works for single-byte character sets (like ASCII).
 * For multi-byte characters (like Chinese), please use `codec.utf8HexToStr`.
 * @param {string} str The hexadecimal string to be converted
 * @returns {string} The real string
 */
dxCommonUtils.codec.hexToStr = function (str) {
    let regex = /.{2}/g;
    let arr = str.match(regex);
    arr = arr.map(item => String.fromCharCode(parseInt(item, 16)));
    return arr.join("");
}
/**
 * Convert a string to a UTF-8 encoded hexadecimal string
 * @param {string} str 
 * @returns {string}
 */
dxCommonUtils.codec.strToUtf8Hex = function (str) {
    const bytes = [];
    for (let i = 0; i < str.length; i++) {
        let code = str.charCodeAt(i);
        if (code < 0x80) {
            bytes.push(code);
        } else if (code < 0x800) {
            bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
        } else if (code < 0xd800 || code >= 0xe000) {
            bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
        } else {
            i++;
            code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
            bytes.push(
                0xf0 | (code >> 18),
                0x80 | ((code >> 12) & 0x3f),
                0x80 | ((code >> 6) & 0x3f),
                0x80 | (code & 0x3f)
            );
        }
    }
    return dxCommonUtils.codec.bytesToHex(bytes);
}
/**
 * Convert the hexadecimal string of utf-8 passed over to a string
 * @param {string} hex Hexadecimal string
 * @returns {string} The real string
 */
dxCommonUtils.codec.utf8HexToStr = function (hex) {
    let array = dxCommonUtils.codec.hexToBytes(hex)
    var out, i, len, c;
    var char2, char3;
    out = "";
    len = array.length;
    i = 0;
    while (i < len) {
        c = array[i++];
        switch (c >> 4) {
            case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                out += String.fromCharCode(c);
                break;
            case 12: case 13:
                char2 = array[i++];
                out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
                break;
            case 14:
                char2 = array[i++];
                char3 = array[i++];
                out += String.fromCharCode(((c & 0x0F) << 12) |
                    ((char2 & 0x3F) << 6) |
                    ((char3 & 0x3F) << 0));
                break;
        }
    }
    return out;
}
/**
 * Convert string to hexadecimal eg: 123abc ->313233616263
 * @description WARNING: This function only works for single-byte character sets (like ASCII).
 * For multi-byte characters (like Chinese), please use `codec.strToUtf8Hex`.
 * @param {string} str The string to be converted
 * @returns {string} Hexadecimal string
 */
dxCommonUtils.codec.strToHex = function (str) {
    if (str === undefined || str === null || typeof (str) != "string") {
        return null
    }
    let val = "";
    for (let i = 0; i < str.length; i++) {
        val += str.charCodeAt(i).toString(16).padStart(2, '0');
    }
    return val
}

/**
 * Convert small format to decimal eg: 001001->69632
 * @param {string} hexString A hexadecimal string in lowercase with no space in between
 * @returns {number} Decimal number
 */
dxCommonUtils.codec.leToDecimal = function (hexString) {
    let reversedHexString = hexString
        .match(/.{2}/g)
        .reverse()
        .join("");
    let decimal = parseInt(reversedHexString, 16);
    return decimal;
}

/**
 * Convert decimal numbers to hexadecimal small format strings
 * @param {number} decimalNumber Decimal digit
 * @param {number} byteSize Generate the number of bytes, defaults to 2
 * @returns {string} Hexadecimal small format string
 */
dxCommonUtils.codec.decimalToLeHex = function (decimalNumber, byteSize) {
    if (decimalNumber === undefined || decimalNumber === null || (typeof decimalNumber) != 'number') {
        throw new Error("dxCommonUtils.codec.decimalToLeHex:'decimalNumber' parameter should be number")
    }
    if (byteSize === undefined || byteSize === null || (typeof byteSize) != 'number' || byteSize <= 0) {
        byteSize = 2
    }
    const littleEndianBytes = [];
    for (let i = 0; i < byteSize; i++) {
        littleEndianBytes.push(decimalNumber & 0xFF);
        decimalNumber >>= 8;
    }
    const littleEndianHex = littleEndianBytes
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .join('');
    return littleEndianHex;
}

/**
 * Convert a hexadecimal string to an ArrayBuffer
 * @param {string} hexString The hexadecimal string to be converted
 * @returns {ArrayBuffer} Converted ArrayBuffer
 */
dxCommonUtils.codec.hexToArrayBuffer = function (hexString) {
    return dxCommonUtils.codec.hexToUint8Array(hexString).buffer;
}

/**
 * Convert hexadecimal string to Uint8Array
 * @param {string} hexString The hexadecimal string to be converted
 * @returns {Uint8Array} Uint8Array object
 */
dxCommonUtils.codec.hexToUint8Array = function (hexString) {
    if (hexString === undefined || hexString === null || (typeof hexString) != 'string' || hexString.length <= 0) {
        throw new Error("dxCommonUtils.codec.hexToUint8Array:'hexString' parameter should not be empty")
    }
    if (!/^[0-9a-fA-F]+$/.test(hexString) || hexString.length % 2 !== 0) {
        throw new Error("dxCommonUtils.codec.hexToUint8Array: 'hexString' parameter must be a valid hex string with an even length");
    }
    let byteString = hexString.match(/.{1,2}/g);
    let byteArray = byteString.map(function (byte) {
        return parseInt(byte, 16);
    });
    let buffer = new Uint8Array(byteArray);
    return buffer;
}

/**
 * Convert ArrayBuffer to hexadecimal string format
 * @param {ArrayBuffer} buffer 
 * @returns {string} A hexadecimal string in lowercase with no space in between
 */
dxCommonUtils.codec.arrayBufferToHex = function (buffer) {
    return dxCommonUtils.codec.uint8ArrayToHex(new Uint8Array(buffer))
}
/**
 * Convert Uint8Array to hexadecimal string format
 * @param {Uint8Array} array 
 * @returns {string} A hexadecimal string in lowercase with no space in between
 */
dxCommonUtils.codec.uint8ArrayToHex = function (array) {
    let hexString = '';
    for (let i = 0; i < array.length; i++) {
        const byte = array[i].toString(16).padStart(2, '0');
        hexString += byte;
    }
    return hexString
}

/**
 * Decodes a Base64 string into an ArrayBuffer.
 * This implementation is robust and handles padding correctly.
 * @memberof dxCommonUtils.codec
 * @param {string} b64 - The Base64 encoded string.
 * @returns {ArrayBuffer} The decoded data as an ArrayBuffer.
 */
dxCommonUtils.codec.base64ToArrayBuffer = function (b64) {
    if (typeof b64 !== 'string') {
        throw new Error("Input must be a Base64 string.");
    }

    // First, clean up any whitespace from the input string.
    const b64_clean = b64.replace(/\s+/g, "");

    // Regex inspired by the js-base64 library to validate the structure of the string.
    // This provides a fast failure for malformed strings.
    const b64re = /^(?:[A-Za-z0-9+/]{4})*?(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
    if (!b64re.test(b64_clean)) {
        throw new Error("Input is not a valid Base64 string (malformed).");
    }

    const B64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    const len = b64_clean.length;

    // This calculation correctly determines the output byte length
    let byte_len = len * 3 / 4;
    if (b64_clean.slice(-2) === '==') {
        byte_len -= 2;
    } else if (b64_clean.slice(-1) === '=') {
        byte_len -= 1;
    }

    const arr = new Uint8Array(byte_len);
    let j = 0;

    for (let i = 0; i < len; i += 4) {
        // Get the index of each base64 character.
        // It will be -1 for '=' or any other invalid character.
        const c1 = B64_MAP.indexOf(b64_clean[i]);
        const c2 = B64_MAP.indexOf(b64_clean[i + 1]);
        const c3 = B64_MAP.indexOf(b64_clean[i + 2]);
        const c4 = B64_MAP.indexOf(b64_clean[i + 3]);

        // Reconstruct the original 3 bytes from the 4 base64 character indices.
        const b1 = (c1 << 2) | (c2 >> 4);
        const b2 = ((c2 & 15) << 4) | (c3 >> 2);
        const b3 = ((c3 & 3) << 6) | c4;

        // Write the first byte, which is always present.
        arr[j++] = b1;

        // Write the second byte if the third base64 character was not a padding character.
        if (c3 !== -1) {
            arr[j++] = b2;
        }

        // Write the third byte if the fourth base64 character was not a padding character.
        if (c4 !== -1) {
            arr[j++] = b3;
        }
    }
    return arr.buffer;
}

/**
 * Encodes an ArrayBuffer into a Base64 string.
 * This implementation is robust and handles padding correctly.
 * @memberof dxCommonUtils.codec
 * @param {ArrayBuffer} buffer - The ArrayBuffer to encode.
 * @returns {string} The Base64 encoded string.
 */
dxCommonUtils.codec.arrayBufferToBase64 = function (buffer) {
    const B64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    const bytes = new Uint8Array(buffer);
    const len = bytes.byteLength;
    let b64 = "";

    for (let i = 0; i < len; i += 3) {
        const b1 = bytes[i];
        const b2 = bytes[i + 1];
        const b3 = bytes[i + 2];

        const c1 = b1 >> 2;
        const c2 = ((b1 & 3) << 4) | (b2 >> 4);
        const c3 = ((b2 & 15) << 2) | (b3 >> 6);
        const c4 = b3 & 63;

        b64 += B64_MAP[c1];
        b64 += B64_MAP[c2];

        if (i + 1 >= len) {
            b64 += "==";
        } else {
            b64 += B64_MAP[c3];
            if (i + 2 >= len) {
                b64 += "=";
            } else {
                b64 += B64_MAP[c4];
            }
        }
    }
    return b64;
}

/**
 * Calculates the BCC (Block Check Character / XOR Checksum) of the input data.
 * @memberof dxCommonUtils.codec
 * @param {string|ArrayBuffer|Uint8Array} data - The data to calculate the checksum for. If a string, it will be treated as UTF-8.
 * @returns {number} The calculated 8-bit BCC value (0-255).
 */
dxCommonUtils.codec.bcc = function (data) {
    const buffer = _normalizeDataToBuffer(data, false, 'Data');
    return utils.calculateBcc(buffer);
}

/**
 * Get disk space usage
 * @returns {object} 
 *          -total: The total disk space in MB.
 *          -used: The used disk space in MB.
 *          -free: The available disk space in MB.
 */
dxCommonUtils.getDiskStats = function () {
    return utils.getDiskStats();
}

/**
 * @private
 * Internal helper to normalize various data inputs into an ArrayBuffer.
 * This function is crucial for ensuring that the native C layer receives data in a consistent format.
 * @param {string|ArrayBuffer|Uint8Array} data The input data.
 * @param {boolean} [isBase64=false] - If the input is a string, specifies if it's Base64 encoded. Defaults to false (UTF-8).
 * @param {string} [paramName='Input'] - The name of the parameter for error messages.
 * @returns {ArrayBuffer}
 */
function _normalizeDataToBuffer(data, isBase64 = false, paramName = 'Input') {
    if (typeof data === 'string') {
        if (isBase64) {
            return dxCommonUtils.codec.base64ToArrayBuffer(data);
        } else {
            // Treat as a UTF-8 string by converting to hex and then to ArrayBuffer.
            return dxCommonUtils.codec.hexToArrayBuffer(dxCommonUtils.codec.strToUtf8Hex(data));
        }
    } else if (data instanceof ArrayBuffer) {
        return data;
    } else if (data instanceof Uint8Array) {
        // Correctly handle views on larger ArrayBuffers by creating a copy of the viewed section.
        // If it's not a view, slice() will create a copy of the buffer.
        return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
    } else {
        throw new Error(`${paramName} must be a string, ArrayBuffer, or Uint8Array`);
    }
}

/**
 * @private
 * Internal helper to normalize crypto inputs, converting Hex strings to ArrayBuffer.
 * @param {string|ArrayBuffer|Uint8Array} data The input data.
 * @param {string} paramName The name of the parameter for error messages.
 * @returns {ArrayBuffer}
 */
function _normalizeHexInput(data, paramName = 'Input') {
    if (typeof data === 'string') {
        return dxCommonUtils.codec.hexToArrayBuffer(data);
    } else if (data instanceof ArrayBuffer) {
        return data;
    } else if (data instanceof Uint8Array) {
        return data.buffer;
    } else {
        throw new Error(`${paramName} must be a hex string, ArrayBuffer, or Uint8Array`);
    }
}

/**
 * Internal helper to convert an ArrayBuffer to a UTF-8 string.
 * This is a workaround for environments that lack a built-in TextDecoder.
 * @param {ArrayBuffer} buffer - The ArrayBuffer to convert.
 * @returns {string} The UTF-8 string.
 */
function _arrayBufferToUtf8String(buffer) {
    if (!buffer || buffer.byteLength === 0) {
        return "";
    }
    // Use the existing, environment-safe codec functions
    const hex = dxCommonUtils.codec.arrayBufferToHex(buffer);
    return dxCommonUtils.codec.utf8HexToStr(hex);
}

/**
 * Gets the native common utils client object.
 * @returns {Object} The native client object.
 */
dxCommonUtils.getNative = function () {
    return utils;
}

export default dxCommonUtils;
